/*******************************************************************************
 * Copyright 2010 Simon Mieth
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package org.kabeja.processing.scripting.impl;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.text.BadLocationException;

import org.kabeja.DraftDocument;
import org.kabeja.processing.AbstractPostProcessor;
import org.kabeja.processing.ProcessorException;
import org.kabeja.ui.DraftDocumentViewComponent;
import org.kabeja.ui.UIException;
import org.kabeja.ui.event.DraftDocumentChangeEventProvider;
import org.kabeja.ui.event.DraftDocumentChangeListener;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

import de.miethxml.toolkit.ui.UIUtils;


public class JavaScriptShell extends AbstractPostProcessor
    implements DraftDocumentViewComponent, DraftDocumentChangeEventProvider {
    protected final static String COMMAND_PREFIX = "js>";
    protected JFrame frame;
    protected JTextArea textArea;
    protected HashMap actions = new HashMap();
    protected ArrayList history = new ArrayList();
    protected int historyPos = 0;
    protected ScriptWorker worker;
    protected String title = "JSShell";
    protected ArrayList listeners = new ArrayList();
    protected DraftDocument doc;

    public void process(DraftDocument doc, Map context) throws ProcessorException {
        worker = new ScriptWorker(doc);
        worker.start();
        this.init();

        // wait for finished shell editing
        while (worker.isAlive()) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void setProperties(Map properties) {
    }

    protected void init() {
        frame = new JFrame(getTitle());
        frame.setJMenuBar(this.createMenuBar());
        frame.getContentPane().setLayout(new BorderLayout());

        frame.addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    // capture the second close-event and ignore
                    dispose();
                }
            });

        frame.getContentPane().add(getView(), BorderLayout.CENTER);

        JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT, 2, 2));
        JButton button = new JButton((Action) actions.get("close"));
        p.add(button);
        frame.getContentPane().add(p, BorderLayout.SOUTH);

        frame.setSize(new Dimension(640, 480));
        frame.setVisible(true);

        newShellLine();
    }

    protected JToolBar createToolbar() {
        JToolBar toolbar = new JToolBar();

        JButton button = new JButton((Action) actions.get("copy"));
        button.setToolTipText(button.getText());
        button.setText("");
        toolbar.add(button);
        button = new JButton((Action) actions.get("paste"));
        button.setToolTipText(button.getText());
        button.setText("");
        toolbar.add(button);
        button = new JButton((Action) actions.get("cut"));
        button.setToolTipText(button.getText());
        button.setText("");
        toolbar.add(button);

        button = new JButton((Action) actions.get("reload"));
        button.setToolTipText(button.getText());
        button.setText("");
        toolbar.add(button);

        return toolbar;
    }

    protected JMenuBar createMenuBar() {
        JMenuBar menubar = new JMenuBar();
        JMenu menu = new JMenu("File");

        JMenuItem item = new JMenuItem((Action) actions.get("open"));
        item.setToolTipText(item.getText());

        menu.add(item);
        item = new JMenuItem((Action) actions.get("save"));
        menu.add(item);
        menu.add(new JSeparator());
        item = new JMenuItem((Action) actions.get("close"));
        menu.add(item);

        menubar.add(menu);

        // menu = new JMenu("Edit");
        // item = new JMenuItem((Action) actions.get("copy"));
        // menu.add(item);
        // item = new JMenuItem((Action) actions.get("paste"));
        // menu.add(item);
        // item = new JMenuItem((Action) actions.get("cut"));
        // menu.add(item);
        // menubar.add(menu);
        return menubar;
    }

    protected void initActions() {
        Action action = new AbstractAction("Cut",
                new ImageIcon(UIUtils.resourceToBytes(this.getClass(),
                        "/icons/cut_edit.gif"))) {
                public void actionPerformed(ActionEvent e) {
                    textArea.cut();
                }
            };

        actions.put("cut", action);

        action = new AbstractAction("Paste",
                new ImageIcon(UIUtils.resourceToBytes(this.getClass(),
                        "/icons/paste_edit.gif"))) {
                    public void actionPerformed(ActionEvent e) {
                        textArea.paste();
                    }
                };
        actions.put("paste", action);

        action = new AbstractAction("Copy",
                new ImageIcon(UIUtils.resourceToBytes(this.getClass(),
                        "/icons/copy_edit.gif"))) {
                    public void actionPerformed(ActionEvent e) {
                        textArea.copy();
                    }
                };
        actions.put("copy", action);

        action = new AbstractAction("Close") {
                    public void actionPerformed(ActionEvent e) {
                        dispose();
                    }
                };
        actions.put("close", action);

        action = new AbstractAction("Save") {
                    public void actionPerformed(ActionEvent e) {
                        JFileChooser fs = new JFileChooser();
                        int r = fs.showSaveDialog(frame);

                        if (r == JFileChooser.APPROVE_OPTION) {
                            try {
                                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
                                            new FileOutputStream(
                                                fs.getSelectedFile())));
                                Iterator i = history.iterator();

                                while (i.hasNext()) {
                                    out.write((String) i.next());
                                    out.newLine();
                                }

                                out.flush();
                                out.close();
                            } catch (IOException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                };
        actions.put("save", action);

        action = new AbstractAction("Open") {
                    public void actionPerformed(ActionEvent e) {
                        textArea.setText(COMMAND_PREFIX);
                        history.clear();

                        JFileChooser fs = new JFileChooser();
                        int r = fs.showOpenDialog(frame);

                        if (r == JFileChooser.APPROVE_OPTION) {
                            try {
                                BufferedReader in = new BufferedReader(new InputStreamReader(
                                            new FileInputStream(
                                                fs.getSelectedFile())));
                                String line;

                                while ((line = in.readLine()) != null) {
                                    textArea.append(line);
                                    evalString(COMMAND_PREFIX + line);
                                }

                                in.close();
                            } catch (IOException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                };
        actions.put("open", action);
        action = new AbstractAction("Update views",
                new ImageIcon(UIUtils.resourceToBytes(this.getClass(),
                        "/icons/reload.gif"))) {
                    public void actionPerformed(ActionEvent e) {
                        fireDXFDocumentChangeEvent();
                    }
                };
        actions.put("reload", action);
    }

    protected String getLineAtCaretPosition() {
        try {
            int lineNumber = this.textArea.getLineOfOffset(this.textArea.getCaretPosition());
            int startOffset = this.textArea.getLineStartOffset(lineNumber);
            int length = this.textArea.getCaretPosition() - startOffset;
            String line = this.textArea.getText(startOffset, length);

            return line;
        } catch (BadLocationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return "";
    }

    protected int getStartOffsetAtCaretPosition() {
        try {
            int lineNumber = textArea.getLineOfOffset(textArea.getCaretPosition());
            int startOffset = textArea.getLineStartOffset(lineNumber);

            return startOffset;
        } catch (BadLocationException e) {
            e.printStackTrace();
        }

        return 0;
    }

    protected void evalString(String line) {
        textArea.append("\n");

        if (line.startsWith(COMMAND_PREFIX)) {
            String script = line.substring(COMMAND_PREFIX.length());

            history.add(script);
            worker.executeScript(script);
        } else {
            newShellLine();
        }
    }

    protected void newShellLine() {
        textArea.append(COMMAND_PREFIX);
        textArea.setCaretPosition(textArea.getDocument().getLength());
    }

    protected void write(String str) {
        try {
            this.textArea.getDocument()
                         .insertString(this.textArea.getCaretPosition(), str,
                null);
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
    }

    protected void dispose() {
        frame.setVisible(false);
        frame.dispose();
        worker.dispose();
    }

    public void showDraftDocument(DraftDocument doc) throws UIException {
    	
        this.doc = doc;
        worker = new ScriptWorker(doc);
        worker.start();
    }

    public String getTitle() {
        return this.title;
    }

    public JComponent getView() {
        textArea = new JTextArea();

        textArea.addKeyListener(new CommandKeyListener());

        this.initActions();

        JPanel panel = new JPanel(new BorderLayout());

        panel.add(new JScrollPane(textArea), BorderLayout.CENTER);

        panel.add(createToolbar(), BorderLayout.NORTH);
        panel.setPreferredSize(new Dimension(640, 480));

        newShellLine();

        worker = new ScriptWorker(null);
        worker.start();

        return panel;
    }

    public void addDraftDocumentChangeListener(DraftDocumentChangeListener listener) {
        this.listeners.add(listener);
    }

    public void removeDraftDocumentChangeListener(
        DraftDocumentChangeListener listener) {
        this.listeners.remove(listener);
    }

    protected void fireDXFDocumentChangeEvent() {
        if (this.doc != null) {
            Iterator i = ((ArrayList) this.listeners.clone()).iterator();

            while (i.hasNext()) {
                DraftDocumentChangeListener l = (DraftDocumentChangeListener) i.next();
                l.changed(this.doc);
            }
        }
    }

    public class CommandKeyListener extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                evalString(getLineAtCaretPosition());
                e.consume();
                historyPos = history.size() - 1;
            } else if ((e.getKeyCode() == KeyEvent.VK_BACK_SPACE) ||
                    (e.getKeyCode() == KeyEvent.VK_LEFT)) {
                try {
                    int lineNumber = textArea.getLineOfOffset(textArea.getCaretPosition());
                    int startOffset = textArea.getLineStartOffset(lineNumber);
                    int l = textArea.getCaretPosition() - startOffset;

                    if (l <= COMMAND_PREFIX.length()) {
                        e.consume();
                    }
                } catch (BadLocationException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }
            } else if (e.getKeyCode() == KeyEvent.VK_UP) {
                if ((historyPos >= 0) && (history.size() > 0)) {
                    if ((historyPos >= history.size()) && (history.size() > 1)) {
                        historyPos = history.size() - 2;
                    }

                    int start = getStartOffsetAtCaretPosition() +
                        COMMAND_PREFIX.length();
                    textArea.replaceRange((String) history.get(historyPos),
                        start, textArea.getDocument().getLength());
                    historyPos--;
                }

                e.consume();
            } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                if ((historyPos < history.size()) && (history.size() > 0)) {
                    if ((historyPos < 0) && (history.size() > 1)) {
                        historyPos = 1;
                    }

                    int start = getStartOffsetAtCaretPosition() +
                        COMMAND_PREFIX.length();
                    textArea.replaceRange((String) history.get(historyPos),
                        start, textArea.getDocument().getLength());
                    historyPos++;
                }

                e.consume();
            }
        }
    }

    protected class ScriptWorker extends Thread {
        protected Context ctx;
        protected Scriptable scope;
        protected boolean execute = false;
        protected boolean dispose = false;
        protected String script;
        protected DraftDocument doc;
        PrintStream err;
        PrintStream out;
        protected PrintStream output = new PrintStream(new JTextAreaPrintWriter());

        public ScriptWorker(DraftDocument doc) {
            this.doc = doc;
        }

        protected void init() {
            // this.ctx = ContextFactory.getGlobal().enter();
            ContextFactory f = new ContextFactory();

            this.ctx = f.enter();
            err = System.err;
            out = System.out;
            // System.setOut(output);
            // System.setErr(output);
            this.scope = ctx.initStandardObjects(null, false);

            try {
                ScriptableObject.defineClass(this.scope, Global.class);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            this.scope = new Global();
            Global.setOutput(output);
            ctx.setErrorReporter(new ErrorReporter() {
                    public void error(String arg0, String arg1, int arg2,
                        String arg3, int arg4) {
                        textArea.append(arg0 + " from line:" + arg3 + "\n");
                    }

                    public EvaluatorException runtimeError(String arg0,
                        String arg1, int arg2, String arg3, int arg4) {
                        textArea.append(arg0 + " from line:" + arg3 + "\n");

                        return new EvaluatorException(arg0);
                    }

                    public void warning(String arg0, String arg1, int arg2,
                        String arg3, int arg4) {
                        textArea.append(arg0 + " from line:" + arg3 + "\n");
                    }
                });

            Object jsOut = Context.javaToJS(doc, scope);
            ScriptableObject.putProperty(scope, "dxf", jsOut);
            textArea.setText("");
            if(doc!=null){
            textArea.append("DXFDocument available as 'dxf'\n");
            }
            newShellLine();
        }

        public void run() {
            init();

            while (!dispose) {
                if (execute) {
                    try {
                        Object result = this.ctx.evaluateString(scope, script,
                                "<cmd>", 1, null);
                        String r = Context.toString(result);

                        if (!r.equals("undefined")) {
                            synchronized (textArea) {
                                textArea.append(r);
                                textArea.append("\n");
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace(output);
                        init();
                    }

                    execute = false;
                    newShellLine();
                } else {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

            Context.exit();
        }

        public void executeScript(String str) {
            this.script = str;
            this.execute = true;

            while (execute) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void dispose() {
            System.setErr(err);
            System.setOut(out);
            this.dispose = true;
        }
    }

    protected class JTextAreaPrintWriter extends OutputStream {
        StringBuffer buf = new StringBuffer();

        public void write(int b) throws IOException {
            if (b == '\r') {
                return;
            } else if (b == '\n') {
                synchronized (textArea) {
                    textArea.append(buf.toString() + "\n");
                }

                buf.setLength(0);
            } else {
                buf.append((char) b);
            }
        }
    }
}
